/*
    jWidget Canvas 0.2.7
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

/*
    JW.Canvas package index file.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas = {};

/*
    JW.Canvas base component implementation.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Component = JW.ObservableConfig.extend({
    EVENT_MOUSEDOWN : "mousedown", // handler(event:JW.Canvas.Event.Mouse)
    EVENT_MOUSEUP   : "mouseup",   // handler(event:JW.Canvas.Event.Mouse)
    EVENT_MOUSEMOVE : "mousemove", // handler(event:JW.Canvas.Event.Mouse)
    EVENT_CLICK     : "click",     // handler(event:JW.Canvas.Event.Mouse)
    EVENT_MOUSEOVER : "mouseover", // handler(event:JW.Event)
    EVENT_MOUSEOUT  : "mouseout",  // handler(event:JW.Event)
    
    x               : 0,        // [optional] Number
    y               : 0,        // [optional] Number
    scaleX          : 1,        // [optional] Number
    scaleY          : 1,        // [optional] Number
    rotate          : 0,        // [optional] Number
    width           : 0,        // [optional] Number
    height          : 0,        // [optional] Number
    
    plugins         : null,     // [optional] Array of JW.Canvas.Plugin
    
    visible         : true,     // [optional] Boolean
    clip            : false,    // [optional] Boolean
    mouseClip       : true,     // [optional] Boolean
    mouseEnabled    : true,     // [optional] Boolean
    mouseChildren   : true,     // [optional] Boolean
    
    // The next options correspond to addChild method specification
    renderParent    : null,     // [optional] JW.Canvas.Component
    renderPosition  : null,     // [optional] Number
    
    parent          : null,     // [readonly] JW.Canvas.Component
    stage           : null,     // [readonly] JW.Canvas.Stage
    graphics        : null,     // [readonly] JW.Canvas.Graphics
    appended        : false,    // [readonly] Boolean
    destroyed       : false,    // [readonly] Boolean
    
    children        : null,     // [readonly] Array of JW.Canvas.Component
    
    transform       : null,     // [readonly] JW.Canvas.Transform
    backTransform   : null,     // [readonly] JW.Canvas.Transform
    
    init: function(config)
    {
        this._super(config);
        
        this._initComponent();
        this._render();
    },
    
    destroy: function()
    {
        if (this.destroyed)
            return;
        
        this.remove();
        
        var plugins  = this.plugins;
        var children = this.children;
        
        this.plugins = [];
        this.removeChildren();
        
        JW.eachByMethod(plugins,  "destroy");
        JW.eachByMethod(children, "destroy");
        
        this.destroyComponent();
        
        delete this.plugins;
        delete this.children;
        delete this.renderParent;
        delete this.stage;
        
        this._super();
    },
    
    /**
     * Component initialization.
     * Override this to specify initial values for component properties.
     * You can build plugins array here, these plugins will prepend ones from config object.
     * Append superclass method call.
     */
    initComponent: function()
    {
    },
    
    /**
     * Component rendering.
     * Override this to render component's HTML/Graphics.
     * Template is applied here and all elements are defined.
     * You can customize elements' properties in this method and finish rendering.
     * Prepend superclass method call.
     */
    render: function()
    {
    },
    
    /**
     * After append to DOM.
     * Override this to specify any initialization actions that require element
     * to be inserted into DOM.
     * Use renderParent or addChild EVERYWHERE to get this work.
     * Prepend superclass method call.
     */
    afterAppend: function()
    {
    },
    
    /**
     * Layout the component.
     * Use renderParent or addChild EVERYWHERE to get this work.
     * Prepend superclass method call.
     */
    doLayout: function()
    {
    },
    
    /**
     * Layout component totally.
     */
    doLayoutAll: function()
    {
        if (!this.stage)
            return;
        
        this.doLayout();
        JW.eachByMethod(this.plugins,  "doLayout");
        JW.eachByMethod(this.children, "doLayoutAll");
    },
    
    /**
     * Component destructor.
     */
    destroyComponent: function()
    {
    },
    
    /**
     * Hit testing function. Tests rectangle (0, 0) - (w, h) by default.
     * Override to specify custom hit testing.
     */
    hitTest: function( // Boolean
        point) // Array[2]
    {
        return this.mouseClip ? this._inRect(point) : true;
    },
    
    /**
     * Get plugin instance by its xtype.
     */
    getPlugin: function(xtype)
    {
        return this.plugins.searchBy("xtype", xtype);
    },
    
    /**
     * Add child. Opposite to appendTo.
     */
    addChild: function(
        cmp,   // [required] JW.Canvas.Component
        index) // [optional] Number
    {
        index = JW.defn(index, this.children.length);
        
        cmp.remove();
        
        this.children.splice(index, 0, cmp);
        
        if (this.stage)
        {
            var stage = this.stage;
            function stageRec(c)
            {
                c.stage = stage;
                c.children.each(stageRec);
            }
            
            stageRec(cmp);
        }
        
        cmp.parent = this;
        cmp._afterAppend();
    },
    
    /**
     * Remove component from the parent and DOM.
     */
    remove: function()
    {
        if (this.parent)
        {
            this.parent._removeChild(this);
            delete this.parent;
            delete this.stage;
        }
    },
    
    /**
     * Remove all children.
     */
    removeChildren: function()
    {
        var children = this.children;
        
        this.children.each(function(cmp) {
            delete cmp.parent;
        }, this);
        
        this.children = [];
        
        return children;
    },
    
    /**
     * Removes child at specified position and returns one.
     */
    removeChild: function(index)
    {
        var cmp = this.children[index];
        cmp.remove();
        return cmp;
    },
    
    /**
     * Get all children.
     */
    getChildren: function()
    {
        return this.children.concat();
    },
    
    /**
     * Get DOM branch (from body to this).
     */
    getBranch: function()
    {
        var result = [];
        function rec(cmp)
        {
            if (!cmp)
                return;
            
            rec(cmp.parent);
            result.push(cmp);
        }
        
        rec(this);
        return result;
    },
    
    /**
     * Set component coordinates.
     */
    setX: function(x)
    {
        this.x = x;
        this._invalidateTransform();
    },
    
    /**
     * Set component coordinates.
     */
    setY: function(y)
    {
        this.y = y;
        this._invalidateTransform();
    },
    
    /**
     * Set component coordinates.
     */
    setXY: function(x, y)
    {
        this.x = x;
        this.y = y;
        this._invalidateTransform();
    },
    
    /**
     * Set component rotation.
     */
    setRotate: function(value)
    {
        this.rotate = value;
        this._invalidateTransform();
    },
    
    /**
     * Set component scale.
     */
    setScaleX: function(x)
    {
        this.scaleX = x;
        this._invalidateTransform();
    },
    
    /**
     * Set component scale.
     */
    setScaleY: function(y)
    {
        this.scaleY = y;
        this._invalidateTransform();
    },
    
    /**
     * Set component scale.
     */
    setScale: function(x, y)
    {
        y = JW.defn(y, x);
        this.scaleX = x;
        this.scaleY = y;
        this._invalidateTransform();
    },
    
    /**
     * Set component size.
     */
    setWidth: function(w)
    {
        this.width = w;
    },
    
    /**
     * Set component size.
     */
    setHeight: function(h)
    {
        this.height = h;
    },
    
    /**
     * Set component size.
     */
    setSize: function(w, h)
    {
        h = JW.defn(h, w);
        this.width  = w;
        this.height = h;
    },
    
    /**
     * Set component visibility.
     */
    setVisible: function(value)
    {
        this.visible = value;
    },
    
    /**
     * Set component clipping.
     */
    setClip: function(value)
    {
        this.clip = value;
    },
    
    /**
     * Set component mouse clipping.
     */
    setMouseClip: function(value)
    {
        this.mouseClip = value;
    },
    
    /**
     * Set component mouse events enabled.
     */
    setMouseEnabled: function(value)
    {
        this.mouseEnabled = value;
    },
    
    /**
     * Set component children mouse events enabled.
     */
    setMouseChildren: function(value)
    {
        this.mouseChildren = value;
    },
    
    /**
     * Get transformation matrix from global to local.
     */
    getTransform: function()
    {
        if (this.transform)
            return this.transform;
        
        var parentTransform = this.parent ? this.parent.getTransform() : JW.Canvas.Transform.identity;
        this.transform = parentTransform.complex(this.x, this.y, this.scaleX, this.scaleY, this.rotate);
        return this.transform;
    },
    
    /**
     * Get transformation matrix from local to global.
     */
    getBackTransform: function()
    {
        if (this.backTransform)
            return this.backTransform;
        
        this.backTransform = this.getTransform().back();
        return this.backTransform;
    },
    
    /**
     * Convert coordinates from global to local.
     */
    globalToLocal: function(point)
    {
        return this.getBackTransform().convert(point);
    },
    
    /**
     * Convert coordinates from local to global.
     */
    localToGlobal: function(point)
    {
        return this.getTransform().convert(point);
    },
    
    _draw: function()
    {
        if (!this.visible)
            return;
        
        var context = this.stage.context;
        
        context.save();
        
        context.translate(this.x, this.y);
        context.rotate(this.rotate);
        context.scale(this.scaleX, this.scaleY);
        
        if (this.clip)
        {
            context.beginPath();
            context.rect(0, 0, this.width, this.height);
            context.closePath();
            context.clip();
        }
        
        this._drawGraphics();
        this.children.eachByMethod("_draw");
        
        context.restore();
    },
    
    _drawGraphics: function()
    {
        var context = this.stage.context;
        var pathBegun = false;
        
        function drawItem(item) {
            item.draw(context);
            
            // close first to handle 2 consequtive beginPath calls
            if (item.isClosePath)
                pathBegun = false;
            
            if (item.isBeginPath)
                pathBegun = true;
        }
        
        context.save();
        this.graphics.items.each(drawItem, this);
        if (pathBegun)
            drawItem(new JW.Canvas.Graphics.Item.ClosePath());
        context.restore();
    },
    
    _processMouseEvent: function(mouseEventProcessor)
    {
        if (!this.visible || !this.mouseEnabled)
            return;
        
        var local = this.globalToLocal(mouseEventProcessor.global);
        if (this.hitTest(local))
            mouseEventProcessor.target = this;
        
        if (this.clip && !this._inRect(local))
            return;
        
        if (!this.mouseChildren)
            return;
        
        this.children.eachByMethod("_processMouseEvent", [ mouseEventProcessor ]);
    },
    
    _invalidateTransform: function()
    {
        if (!this.transform)
            return;
        
        delete this.transform;
        delete this.backTransform;
        
        this.children.eachByMethod("_invalidateTransform");
    },
    
    _initComponent: function()
    {
        var plugins = this.plugins || [];
        this.plugins = [];
        this.children = [];
        
        this.initComponent();
        
        this.plugins = this.plugins.concat(plugins);
        for (var i = 0; i < this.plugins.length; ++i)
            this.plugins[i].attach(this);
    },
    
    _render: function()
    {
        this.graphics = new JW.Canvas.Graphics();
        
        this.render();
        for (var i = 0; i < this.plugins.length; ++i)
            this.plugins[i].render();
        
        this._applyRenderTo();
    },
    
    _applyRenderTo: function()
    {
        if (this.renderParent)
            this.renderParent.addChild(this, this.renderPosition);
        
        this._afterAppend();
    },
    
    _afterAppend: function()
    {
        if (this.appended || this.destroyed || !this.stage)
            return;
        
        this.appended = true;
        this.afterAppend();
        JW.eachByMethod(this.children, "_afterAppend");
        JW.eachByMethod(this.plugins,   "afterAppend");
        
        this.doLayoutAll();
    },
    
    _removeChild: function(cmp)
    {
        this.children.removeItem(cmp);
    },
    
    _inRect: function(point)
    {
        return (point[0] >= 0) && (point[0] <= this.width) &&
               (point[1] >= 0) && (point[1] <= this.height);
    }
});

/*
    JW.Canvas body component implementation.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Body = JW.Canvas.Component.extend({
    initComponent: function()
    {
        this.width  = this.stage.width;
        this.height = this.stage.height;
        
        this._super();
    },
    
    hitTest: function()
    {
        return true;
    }
});

/*
    JW.Canvas event model.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Event = JW.Class.extend({
    type          : null, // [readonly] String
    originalEvent : null, // [readonly] jQuery event
    target        : null, // [readonly] JW.Canvas.Component
    currentTarget : null, // [readonly] JW.Canvas.Component
    
    isBubble      : true, // [readonly] Boolean
    isDefault     : true, // [readonly] Boolean
    
    init: function(originalEvent, target)
    {
        this.type = originalEvent.type;
        this.originalEvent = originalEvent;
        this.target = target;
    },
    
    stopPropagation: function()
    {
        this.isBubble = false;
    },
    
    preventDefault: function()
    {
        this.isDefault = false;
    },
    
    _setCurrentTarget: function(target)
    {
        this.currentTarget = target;
    }
});

/*
    JW.Canvas mouse event model.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Event.Mouse = JW.Canvas.Event.extend({
    local  : null, // [readonly] Array[2]
    global : null, // [readonly] Array[2]
    
    init: function(originalEvent, target, global, type)
    {
        this._super(originalEvent, target);
        this.global = global;
        this.type = type || this.type;
    },
    
    _setCurrentTarget: function(target)
    {
        this._super(target);
        
        if (target)
            this.local = target.globalToLocal(this.global);
    }
});

/*
    JW.Canvas mouse event processor.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Event.Mouse.Processor = JW.Class.extend({
    global : null, // [readonly] Array[2]
    target : null, // [readonly] JW.Canvas.Component
    
    init: function(globalX, globalY)
    {
        this.global = [ globalX, globalY ];
    }
});

/*
    JW.Canvas component graphics model.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    
    ----
    
    See http://www.w3.org/TR/2dcontext for canvas 2D context specification.
*/

JW.Canvas.Graphics = JW.Class.extend({
    items : null, // Array of JW.Canvas.Graphics.Item
    
    init: function()
    {
        this.items = [];
    },
    
    save: function()
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("save"));
    },
    
    restore: function()
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("restore"));
    },
    
    scale: function(x, y)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("scale", [ x, y ]));
    },
    
    rotate: function(angle)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("rotate", [ angle ]));
    },
    
    translate: function(x, y)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("translate", [ x, y ]));
    },
    
    transform: function(a, b, c, d, e, f)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("transform", [ a, b, c, d, e, f ]));
    },
    
    flipX: function(x)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("transform", [ -1, 0, 0, 1, 2 * x, 0 ]));
    },
    
    flipY: function(y)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("transform", [ 1, 0, 0, -1, 0, 2 * y ]));
    },
    
    // TODO: test
    flip: function(a, r)
    {
        var sin  = Math.sin(a);
        var cos  = Math.cos(a);
        var sin2 = Math.sin(2 * a);
        var cos2 = Math.cos(2 * a);
        
        this.items.push(new JW.Canvas.Graphics.Item.Method("transform", [ -cos2, -sin2, -sin2, cos2, 2 * r * cos, 2 * r * sin ]));
    },
    
    setTransform: function(a, b, c, d, e, f)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("setTransform", [ a, b, c, d, e, f ]));
    },
    
    globalAlpha: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("globalAlpha", value));
    },
    
    strokeStyle: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("strokeStyle", value));
    },
    
    fillStyle: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("fillStyle", value));
    },
    
    lineWidth: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("lineWidth", value));
    },
    
    lineCap: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("lineCap", value));
    },
    
    lineJoin: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("lineJoin", value));
    },
    
    miterLimit: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("miterLimit", value));
    },
    
    shadowOffsetX: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("shadowOffsetX", value));
    },
    
    shadowOffsetY: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("shadowOffsetY", value));
    },
    
    shadowBlur: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("shadowBlur", value));
    },
    
    shadowColor: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("shadowColor", value));
    },
    
    fillRect: function(x, y, w, h)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("fillRect", [ x, y, w, h ]));
    },
    
    strokeRect: function(x, y, w, h)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("strokeRect", [ x, y, w, h ]));
    },
    
    beginPath: function()
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method.Path("beginPath", true));
    },
    
    closePath: function()
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method.Path("closePath", false));
    },
    
    moveTo: function(x, y)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("moveTo", [ x, y ]));
    },
    
    lineTo: function(x, y)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("lineTo", [ x, y ]));
    },
    
    quadraticCurveTo: function(cpx, cpy, x, y)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("quadraticCurveTo", [ cpx, cpy, x, y ]));
    },
    
    bezierCurveTo: function(cp1x, cp1y, cp2x, cp2y, x, y)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("bezierCurveTo", [ cp1x, cp1y, cp2x, cp2y, x, y ]));
    },
    
    arcTo: function(x1, y1, x2, y2, radius)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("arcTo", [ x1, y1, x2, y2, radius ]));
    },
    
    rect: function(x, y, w, h)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("rect", [ x, y, w, h ]));
    },
    
    arc: function(x, y, radius, startAngle, endAngle, anticlockwise)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("arc", [ x, y, radius, startAngle, endAngle, anticlockwise ]));
    },
    
    fill: function()
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("fill"));
    },
    
    stroke: function()
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("stroke"));
    },
    
    font: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("font", value));
    },
    
    textAlign: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("textAlign", value));
    },
    
    textBaseline: function(value)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Property("textBaseline", value));
    },
    
    fillText: function(text, x, y, maxWidth)
    {
        // Temporary hack due to http://code.google.com/p/chromium/issues/detail?id=110995
        if (JW.isSet(maxWidth))
            this.items.push(new JW.Canvas.Graphics.Item.Method("fillText", [ text, x, y, maxWidth ]));
        else
            this.items.push(new JW.Canvas.Graphics.Item.Method("fillText", [ text, x, y ]));
    },
    
    strokeText: function(text, x, y, maxWidth)
    {
        // Temporary hack due to http://code.google.com/p/chromium/issues/detail?id=110995
        if (JW.isSet(maxWidth))
            this.items.push(new JW.Canvas.Graphics.Item.Method("strokeText", [ text, x, y, maxWidth ]));
        else
            this.items.push(new JW.Canvas.Graphics.Item.Method("strokeText", [ text, x, y ]));
    },
    
    measureText: function(text)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("measureText", [ text ]));
    },
    
    drawImage: function(image, sx, sy, sw, sh, dx, dy, dw, dh)
    {
        this.items.push(new JW.Canvas.Graphics.Item.Method("drawImage", [ image, sx, sy, sw, sh, dx, dy, dw, dh ]));
    },
    
    clear: function()
    {
        this.items.splice(0, this.items.length);
    }
});

/*
    JW.Canvas component graphics model base item.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Graphics.Item = JW.Class.extend({
    isBeginPath : false,
    isClosePath : false,
    
    draw: function(context)
    {
    }
});

/*
    JW.Canvas component graphics model base method item.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Graphics.Item.Method = JW.Canvas.Graphics.Item.extend({
    method : null,
    args   : null,
    
    init: function(method, args)
    {
        this.method = method;
        this.args   = args;
    },
    
    draw: function(context)
    {
        context[this.method].apply(context, this.args);
    }
});

/*
    JW.Canvas component graphics model path method item.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Graphics.Item.Method.Path = JW.Canvas.Graphics.Item.Method.extend({
    isClosePath : true,
    
    init: function(method, isBeginPath)
    {
        this._super(method);
        this.isBeginPath = isBeginPath;
    }
});

/*
    JW.Canvas component graphics model base property item.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Graphics.Item.Property = JW.Canvas.Graphics.Item.extend({
    property : null,
    value    : null,
    
    init: function(property, value)
    {
        this.property = property;
        this.value    = value;
    },
    
    draw: function(context)
    {
        context[this.property] = this.value;
    }
});

/*
    JW base plugin to canvas components.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Plugin = JW.ObservableConfig.extend({
    xtype   : null, // [readonly] String
    target  : null, // [readonly] JW.Canvas.Component to attach plugin to
    
    attach: function(target)
    {
        this.target = target;
        this.initComponent();
    },
    
    destroy: function()
    {
        this.destroyComponent();
        this._super();
    },
    
    // virtual
    initComponent: function()
    {
    },
    
    // virtual
    render: function()
    {
    },
    
    // virtual
    afterAppend: function()
    {
    },
    
    // virtual
    doLayout: function()
    {
    },
    
    // virtual
    destroyComponent: function()
    {
    }
});

/*
    JW.Canvas stage - canvas HTML element adapter for JW.UI.Component.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Stage = JW.UI.Component.extend({
    width       : null,  // [required] Number
    height      : null,  // [required] Number
    fps         : 25,    // [optional] Number
    bodyCls     : null,  // [optional] JW.Canvas.Body subclass
    bodyCfg     : null,  // [optional] Object
    stopOnError : false, // [optional] Boolean
    
    context     : null,  // [readonly] CanvasRenderingContext2D
    body        : null,  // [readonly] this.bodyCls
    mouseBranch : null,  // [readonly] Array of JW.Canvas.Component
    mdownBranch : null,  // [readonly] Array of JW.Canvas.Component
    timer       : null,  // [readonly] JW.Timer
    
    render: function()
    {
        this._super();
        
        this.el.attr({
            width  : this.width,
            height : this.height
        });
        
        this.context = this.el[0].getContext("2d");
        
        this.initBody();
        
        this.mouseBranch = [];
        this.mdownBranch = [];
        
        this.timer = new JW.Timer(1000 / this.fps, true);
        this.timer.bind("tick", this._onTimer, this);
        
        this.el.bind("mousedown", this._onMouseEvent.inScope(this));
        this.el.bind("mouseup",   this._onMouseEvent.inScope(this));
        this.el.bind("mousemove", this._onMouseEvent.inScope(this));
        this.el.bind("mouseout",  this._onMouseOut.inScope(this));
    },
    
    initBody: function()
    {
        var cls = this.bodyCls || JW.Canvas.Component.Body;
        var cfg = JW.apply({}, this.bodyCfg);
        cfg.stage = this;
        this.body = new cls(cfg);
    },
    
    afterAppend: function()
    {
        this._super();
        
        this.timer.start();
    },
    
    destroyComponent: function()
    {
        this.timer.stop();
        
        this._super();
    },
    
    _draw: function()
    {
        this.context.clearRect(0, 0, this.width, this.height);
        this.body._draw();
    },
    
    _onTimer: function()
    {
        if (this.stopOnError)
        {
            if (this._timerHandling)
                return;
            
            this._timerHandling = true;
        }
        
        this._draw();
        
        if (this.stopOnError)
            delete this._timerHandling;
    },
    
    _onMouseEvent: function(event)
    {
        event.preventDefault();
        
        var mouseEventProcessor = this._processMouseEvent(event);
        
        var target = mouseEventProcessor.target;
        var global = mouseEventProcessor.global;
        
        this._setMouseTarget(target);
        this._triggerMouseEvent(event, target, global);
        
        if (event.type === "mousedown")
            this.mdownBranch = this.mouseBranch.concat();
        
        if (event.type === "mouseup")
            this._onMouseUp(event, global);
    },
    
    _onMouseOut: function(event)
    {
        this._setMouseTarget(null);
    },
    
    _onMouseUp: function(event, global)
    {
        var diffIndex = this._getBranchesDiffIndex(this.mdownBranch, this.mouseBranch);
        var target = this.mouseBranch[diffIndex - 1];
        this._triggerMouseEvent(event, target, global, "click");
        this.mdownBranch = [];
    },
    
    _processMouseEvent: function(event)
    {
        var position = this.el.offset();
        var mouseEventProcessor = new JW.Canvas.Event.Mouse.Processor(
            event.pageX - position.left,
            event.pageY - position.top
        );
        
        this.body._processMouseEvent(mouseEventProcessor);
        
        return mouseEventProcessor;
    },
    
    _setMouseTarget: function(target)
    {
        var oldBranch = this.mouseBranch;
        this.mouseBranch = target ? target.getBranch() : [];
        
        var diffIndex = this._getBranchesDiffIndex(oldBranch, this.mouseBranch);
        for (var i = 0; i < oldBranch.length - diffIndex; ++i)
            oldBranch[oldBranch.length - i - 1].trigger("mouseout");
        
        for (var i = diffIndex; i < this.mouseBranch.length; ++i)
            this.mouseBranch[i].trigger("mouseover");
    },
    
    _triggerMouseEvent: function(event, target, global, type)
    {
        if (!target)
            return;
        
        var wasTarget = false;
        var canvasEvent = new JW.Canvas.Event.Mouse(event, target, global, type);
        for (var i = 0; i < this.mouseBranch.length; ++i)
        {
            if (!canvasEvent.isBubble)
                break;
            
            var currentTarget = this.mouseBranch[this.mouseBranch.length - i - 1];
            wasTarget = wasTarget || (currentTarget == target);
            if (!wasTarget)
                continue;
            
            canvasEvent._setCurrentTarget(currentTarget);
            currentTarget.__triggerEvent(canvasEvent, []);
        }
    },
    
    _getBranchesDiffIndex: function(x, y)
    {
        var l = Math.min(x.length, y.length);
        for (var i = 0; i < l; ++i)
        {
            if (x[i] !== y[i])
                return i;
        }
        
        return l;
    }
});

JW.UI.template(JW.Canvas.Stage, {
    main: '<canvas />'
});

/*
    JW.Canvas 2D coordinates system.
    
    Copyright (C) 2011 Egor Nepomnyaschih
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.
    
    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

JW.Canvas.Transform = JW.Class.extend({
    matrix : null, // Array[6]
    // a c e
    // b d f
    // 0 0 1
    
    init: function(matrix)
    {
        this.matrix = matrix || [ 1, 0, 0, 1, 0, 0 ];
    },
    
    convert: function(point)
    {
        return JW.Canvas.Transform.multMatrixVector(this.matrix, point);
    },
    
    complex: function(x, y, scaleX, scaleY, rotate)
    {
        var cos = Math.cos(rotate);
        var sin = Math.sin(rotate);
        return this.transform(scaleX * cos, scaleX * sin, -scaleY * sin, scaleY * cos, x, y);
    },
    
    transform: function(a, b, c, d, e, f)
    {
        return new JW.Canvas.Transform(JW.Canvas.Transform.multMatrix(this.matrix, arguments));
    },
    
    back: function()
    {
        return new JW.Canvas.Transform(JW.Canvas.Transform.backMatrix(this.matrix));
    }
});

JW.apply(JW.Canvas.Transform, {
    identity: new JW.Canvas.Transform(),
    
    multMatrixVector: function(m, u)
    {
        return [
            m[0] * u[0] + m[2] * u[1] + m[4],
            m[1] * u[0] + m[3] * u[1] + m[5]
        ];
    },
    
    multMatrix: function(m, n)
    {
        return [
            m[0] * n[0] + m[2] * n[1],
            m[1] * n[0] + m[3] * n[1],
            m[0] * n[2] + m[2] * n[3],
            m[1] * n[2] + m[3] * n[3],
            m[0] * n[4] + m[2] * n[5] + m[4],
            m[1] * n[4] + m[3] * n[5] + m[5]
        ];
    },
    
    backMatrix: function(m)
    {
        var d = m[0] * m[3] - m[1] * m[2];
        return [
             m[3] / d,
            -m[1] / d,
            -m[2] / d,
             m[0] / d,
            (m[2] * m[5] - m[3] * m[4]) / d,
            (m[1] * m[4] - m[0] * m[5]) / d
         ];
    }
});

